﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics;

namespace ParallelRuntimeLibrary
{
    public class SemaphoreSlim : IDisposable, ISupportsCancellation
    {
        private object mCountLock;
        private volatile int mCurrentCount;
        private int mInitialCount;
        private bool mIsCanceled;
        private readonly int mMaxCount;
        private volatile int mWaitCount;
        private ManualResetEvent mWaitHandle;
        
        private const int NO_MAXIMUM = 0x7fffffff;

        /*////////////////////////////////////////////////////////////////////////////////////////////////*/

        /// <summary>
        /// Initializes a new instance of the <see cref="T:System.Threading.SemaphoreSlim" /> class, specifying
        /// the initial number of requests that can be granted concurrently.
        /// </summary>
        /// <param name="initialCount">The initial number of requests for the semaphore that can be granted concurrently.</param>
        /// <exception cref="T:System.ArgumentOutOfRangeException">The <paramref name="initialCount" />
        /// argument is negative.</exception>
        public SemaphoreSlim(int initialCount)
            : this(initialCount, NO_MAXIMUM)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:System.Threading.SemaphoreSlim" /> class, specifying
        /// the initial and maximum number of requests that can be granted concurrently.
        /// </summary>
        /// <param name="initialCount">The initial number of requests for the semaphore that can be granted concurrently.</param>
        /// <param name="maxCount">The maximum number of requests for the semaphore that can be granted concurrently.</param>
        /// <exception cref="T:System.ArgumentOutOfRangeException">The <paramref name="initialCount" />
        /// argument is negative, <paramref name="initialCount" /> is greater than <paramref name="maxCount" />,
        /// or <paramref name="maxCount" /> is not positive.</exception>
        public SemaphoreSlim(int initialCount, int maxCount)
        {
            if ((initialCount < 0) || (initialCount > maxCount))
                throw new ArgumentOutOfRangeException("initialCount", "the initialCount value shouldn't be less than zero, or more than maxCount");
            if (maxCount <= 0)
                throw new ArgumentOutOfRangeException("maxCount", "The maxCount value shouldn't be equal or less than zero");

            mInitialCount = initialCount;
            mMaxCount = maxCount;
            mCountLock = new object();
            mCurrentCount = mInitialCount;
            mIsCanceled = false;
        }

        /// <summary>
        /// Helper function to measure and update the wait time
        /// </summary>
        /// <param name="stopWatch"> The StopWatch object used to measure the time</param>
        /// <param name="originalWaitTime">The orginal wait time</param>
        /// <returns>The new wait time, -1 if the time expired</returns>
        private static int UpdateTimeOut(Stopwatch stopWatch, int originalWaitTime)
        {
            if (stopWatch.ElapsedMilliseconds > 0x7fffffffL)
            {
                return 0;
            }
            int num = originalWaitTime - ((int)stopWatch.ElapsedMilliseconds);
            if (num <= 0)
            {
                return 0;
            }
            return num;
        }

        /*////////////////////////////////////////////////////////////////////////////////////////////////*/


        /// <summary>
        /// Checks the dispose status by checking the lock object, if it is null means that object
        /// has been disposed and throw ObjectDisposedException
        /// </summary>
        private void CheckDispose()
        {
            if (this.mCountLock == null)
                throw new ObjectDisposedException("SemaphoreSlim is Disposed");
        }

        /// <summary>
        /// Releases the resources used by the <see cref="T:ParallelRuntimeLibrary.SemaphoreSlim" />.
        /// </summary>
        /// <remarks>
        /// <see cref="ParallelRuntimeLibrary.SemaphoreSlim.Dispose" /> should not be called while any threads are waiting on the semaphore, as
        /// whether they are woken when it’s disposed is undefined.
        /// </remarks>
        public void Dispose()
        {
            this.Dispose(true);
        }

        /// <summary>
        /// Releases the resources used by the <see cref="T:ParallelRuntimeLibrary.SemaphoreSlim" />.
        /// </summary>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.mWaitHandle != null)
                {
                    this.mWaitHandle.Close();
                    this.mWaitHandle = null;
                }
                this.mCountLock = null;
            }
        }

        /*///////////////////////////////////////////////////////////////////////////////////*/

        /// <summary>
        /// Exits the semaphore and returns the previous count.
        /// </summary>
        /// <returns>The count on the semaphore before the Release method was called.</returns>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:ParallelRuntimeLibrary.SemaphoreSlim" /> has been disposed.</exception>
        public int Release()
        {
            return this.Release(1);
        }

        /// <summary>
        /// Exits the semaphore a specified number of times and returns the previous count.
        /// </summary>
        /// <param name="releaseCount">The number of times to exit the semaphore.</param>
        /// <returns>The count on the semaphore before the Release method was called.</returns>
        /// <exception cref="T:System.Threading.SemaphoreFullException">The new count of the seamphore would
        /// exceed the maximum count.</exception>
        /// <exception cref="T:System.ArgumentOutOfRangeException">The <paramref name="releaseCount" /> must
        /// be positive.</exception>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.Threading.SemaphoreSlim" /> has been disposed.</exception>
        public int Release(int releaseCount)
        {
            if (releaseCount < 1)
                throw new ArgumentOutOfRangeException("releaseCount", releaseCount, "releaseCount must be postive");
            
            this.CheckDispose();
            CheckCancellation();

            lock (mCountLock)
            {
                if ((mMaxCount - mCurrentCount) < releaseCount)
                    throw new SemaphoreFullException();
                
                mCurrentCount += releaseCount;
                
                if (mCurrentCount == 1 || mWaitCount == 1)
                    Monitor.Pulse(this.mCountLock);                
                else if (mWaitCount > 1)
                    Monitor.PulseAll(this.mCountLock);

                if ((mWaitHandle != null) && ((mCurrentCount - releaseCount) == 0))
                    this.mWaitHandle.Set();
                
                return (mCurrentCount - releaseCount);
            }
        }


        /// <summary>
        /// Decrements <see cref="ParallelRuntimeLibrary.SemaphoreSlim.CurrentCount" /> by one if it greater than zero. If <see cref="ParallelRuntimeLibrary.SemaphoreSlim.CurrentCount" />
        /// is zero, blocks the current thread until it's greater than zero.
        /// </summary>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="ParallelRuntimeLibrary.SemaphoreSlim" /> has been disposed.</exception>
        public void Wait()
        {
            this.Wait(-1);
        }

        /// <summary>
        /// Decrements <see cref="P:ParallelRuntimeLibrary.SemaphoreSlim.CurrentCount" /> by one if it greater than zero. If <see cref="P:ParallelRuntimeLibrary.SemaphoreSlim.CurrentCount" />
        /// is zero, blocks the current thread until <see cref="P:ParallelRuntimeLibrary.SemaphoreSlim.CurrentCount" /> is greater than zero or until
        /// a specified timeout expires.
        /// </summary>
        /// <param name="millisecondsTimeout">The number of milliseconds to wait, or <see cref="F:System.Threading.Timeout.Infinite" /> (-1) to wait indefinitely.</param>
        /// <returns>true if the decrement request was granted within the allotted time; otherwise,
        /// false.</returns>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:ParallelRuntimeLibrary.SemaphoreSlim" /> has been disposed.</exception>
        public bool Wait(int millisecondsTimeout)
        {
            if (millisecondsTimeout < -1)
                throw new ArgumentOutOfRangeException("totalMilliSeconds", "The totalMilliSeconds value must be positive");
            
            this.CheckDispose();
            
            int num = millisecondsTimeout;
            
            Stopwatch stopWatch = null;
            if (millisecondsTimeout != -1)
                stopWatch = Stopwatch.StartNew();
                
            SpinWait wait = new SpinWait();
            while (true)
            {
                if (this.mCurrentCount > 0)
                {
                    CheckCancellation();
                    if (Monitor.TryEnter(this.mCountLock))
                        break;
                }

                if (wait.NextSpinWillYield)
                {
                    if (millisecondsTimeout == 0)
                        return false;
                    
                    if (millisecondsTimeout != -1)
                    {
                        num = UpdateTimeOut(stopWatch, millisecondsTimeout);
                        if (num <= 0)
                            return false;
                    }
                    
                    if (!Monitor.TryEnter(this.mCountLock, num))
                        return false;
                    
                    break;
                }
                wait.SpinOnce();
            }

            this.mWaitCount++;
            
            try
            {
                CheckCancellation();
                while (this.mCurrentCount == 0)
                {
                    if (millisecondsTimeout != -1)
                    {
                        num = UpdateTimeOut(stopWatch, millisecondsTimeout);
                        if (num <= 0)
                        {
                            return false;
                        }
                    }
                    if (!Monitor.Wait(this.mCountLock, num))
                    {
                        return false;
                    }
                    CheckCancellation();
                }
                this.mCurrentCount--;
                if ((this.mWaitHandle != null) && (this.mCurrentCount == 0))
                {
                    this.mWaitHandle.Reset();
                }
            }
            finally
            {
                this.mWaitCount--;
                Monitor.Exit(this.mCountLock);
            }
            return true;
        }

        /// <summary>
        /// Decrements <see cref="P:ParallelRuntimeLibrary.SemaphoreSlim.CurrentCount" /> by one if it greater than zero. If <see cref="P:ParallelRuntimeLibrary.SemaphoreSlim.CurrentCount" />
        /// is zero, blocks the current thread until <see cref="P:ParallelRuntimeLibrary.SemaphoreSlim.CurrentCount" /> is greater than zero or until
        /// a specified timeout expires.
        /// </summary>
        /// <param name="timeout">A <see cref="T:System.TimeSpan" /> that represents the number of milliseconds
        /// to wait, or a <see cref="T:System.TimeSpan" /> that represents -1 milliseconds to wait indefinitely.
        /// </param>
        /// <returns>true if the decrement request was granted within the allotted time; otherwise,
        /// false.</returns>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:ParallelRuntimeLibrary.SemaphoreSlim" /> has been disposed.</exception>
        /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="timeout" /> is a negative
        /// number other than -1 milliseconds, which represents an infinite time-out -or- timeout is greater
        /// than
        /// <see cref="F:System.Int32.MaxValue" />.</exception>
        public bool Wait(TimeSpan timeout)
        {
            long totalMilliseconds = (long)timeout.TotalMilliseconds;
            if ((totalMilliseconds < -1L) || (totalMilliseconds > 0x7fffffffL))
                throw new ArgumentOutOfRangeException("timeout", timeout, "the timeout value must be positive and less than 2147483647");
            
            return this.Wait((int)timeout.TotalMilliseconds);
        }


        /// <summary>
        /// Returns a <see cref="T:System.Threading.WaitHandle" /> that can be used to wait on the semaphore.
        /// </summary>
        /// <value>A <see cref="T:System.Threading.WaitHandle" /> that can be used to wait on the
        /// semaphore.</value>
        /// <remarks>
        /// A successful wait on the <see cref="P:ParallelRuntimeLibrary.SemaphoreSlim.AvailableWaitHandle" /> does not imply a successful wait on
        /// the <see cref="T:ParallelRuntimeLibrary.SemaphoreSlim" /> itself. <see cref="P:ParallelRuntimeLibrary.SemaphoreSlim.AvailableWaitHandle" /> exists
        /// to allow a thread to block waiting on multiple semaphores, but such a wait should be followed by
        /// a true wait on the target semaphore.
        /// </remarks>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.Threading.SemaphoreSlim" /> has been disposed.</exception>
        public WaitHandle AvailableWaitHandle
        {
            get
            {
                this.CheckDispose();
                this.CheckCancellation();
                if (this.mWaitHandle == null)
                {
                    lock (this.mCountLock)
                    {
                        if (this.mWaitHandle == null)
                        {
                            this.mWaitHandle = new ManualResetEvent(this.mCurrentCount != 0);
                        }
                    }
                }
                return this.mWaitHandle;
            }
        }

        /// <summary>
        /// Gets the current count of the <see cref="T:System.Threading.SemaphoreSlim" />.
        /// </summary>
        /// <value>The current count of the <see cref="T:System.Threading.SemaphoreSlim" />.</value>
        public int CurrentCount
        {
            get
            {
                return this.mCurrentCount;
            }
        }

        #region ISupportsCancellation Members

        public void Cancel()
        {
            this.CheckDispose();

            if (!mIsCanceled)
            {
                lock (mCountLock)
                {
                    if (!mIsCanceled)
                    {
                        mIsCanceled = true;
                        if (mWaitCount > 0)
                            Monitor.PulseAll(this.mCountLock);
                        
                    }
                }
            }
        }

        public bool IsCanceled
        {
            get { return mIsCanceled; }
        }

        #endregion

        /// <summary>
        /// Local helper method to check the interruption and throw exception if it is interrupted
        /// </summary>
        private void CheckCancellation()
        {
            if (mIsCanceled)
                throw new OperationCanceledException("SemaphoreSlim is canceled");
        }
    }
}
